package com.hlkj.warboot.module_wx.controller;

import com.alibaba.fastjson.JSONObject;
import com.hlkj.warboot.configurer.util.ResponseUtil;
import com.hlkj.warboot.configurer.util.StrUtil;
import com.hlkj.warboot.module_wx.configurer.event.HkSeckillEvent;
import com.hlkj.warboot.module_wx.entity.HkSeckillEntity;
import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 * 微信号卡秒杀 前端控制器
 * </p>
 *
 * @author lixing_java@163.com
 * @since 2020-04-23
 */
@Controller
@RequestMapping("/wx/hkSeckill")
public class HkSeckillControler {
    private volatile long stock = -1; // 标识商品库存需要被初始化
    private String stockKey = "warboot:wx:stock"; // 商品库存key，type=String
    private String winprizeKey = "warboot:wx:winprize"; // 中奖信息key，type=Set
    private String lockKey = "warboot:wx:lock"; // 分布式锁

    @Resource
    private ApplicationContext applicationContext;

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * @Description TODO 活动首页
     * http://jfgna3.natappfree.cc/warboot/wx/hkSeckill/index
     * @Author lixing
     * @Date 2020-04-23 21:07
     */
    @RequestMapping("index")
    //@BaseWxAuthUtil(isWxChat = false)
    public ModelAndView index(HttpServletRequest request) throws Exception {
        HttpSession httpSession = request.getSession();
        JSONObject userinfo = new JSONObject();
        userinfo.put("openid", httpSession.getAttribute("openid"));
        userinfo.put("nickName", httpSession.getAttribute("nickName"));
        userinfo.put("cover", httpSession.getAttribute("cover"));

        ModelAndView modelAndView = new ModelAndView("wx/hkSeckill/index");
        modelAndView.addObject("userinfo", userinfo);
        return modelAndView;
    }

    /*
     * @Description TODO 秒杀
     *   http://jfgna3.natappfree.cc/warboot/wx/hkSeckill/seckill
     * @Author lixing
     * @Date 2020-04-23 10:12
     * @Param []
     * @return com.alibaba.fastjson.JSONObject
     */
    // 删除分布式锁（每个线程只能删除自己的锁）
    public void deleteLockKey(RedisTemplate redisTemplate, String currentThreadName) {
        if (currentThreadName.equals(redisTemplate.opsForValue().get(lockKey))) {
            redisTemplate.delete(lockKey);
        }
    }

    @RequestMapping("seckill")
    @ResponseBody
    //@BaseAccessLimitUtil(interfaceMaxCount = 3, ipMaxCount = 2, isWxChat = false)
    public JSONObject seckill(HttpServletRequest request) {
        // TODO 判断库存（使用共享内存，减少与redis交互）
        if (stock == 0) {
            return ResponseUtil.fail("奖品已被抢光");
        }
        // TODO 判断当前用户是否中奖，已中过的直接剔除，减少与redis交互
        String userId;
        Object openidObj = request.getSession().getAttribute("openid");
        if (openidObj != null && StrUtil.isEffective(openidObj.toString())) {
            userId = openidObj.toString();
        } else {
            userId = System.currentTimeMillis() + "," + new Random().nextDouble();
        }
        if (redisTemplate.opsForSet().isMember(winprizeKey, userId)) {
            return ResponseUtil.fail("您已被奖品砸过啦");
        }
        /*
         * TODO 分布式锁
         *  不存在则新增并返回true，存在则返回false
         *  设置锁的TTL倒计时为30秒，如果30秒内还没有执行结束则自动释放锁，防止死锁
         *  开启事务机制，通过watch监控库存防止自动释放锁后出现超卖现象
         */
        if (redisTemplate.opsForValue().setIfAbsent(lockKey, Thread.currentThread().getName(), 30, TimeUnit.SECONDS)) {
            // TODO 初始化商品库存
            if (stock == -1) {
                Object stockObj = redisTemplate.opsForValue().get(stockKey);
                if (stockObj == null) {
                    redisTemplate.persist(lockKey); // 设置当前锁长期有效
                    return ResponseUtil.fail("初始化商品库存时出错");
                } else {
                    stock = Long.parseLong(stockObj.toString());
                }
            }
            if (stock <= 0) {
                redisTemplate.persist(lockKey); // 设置当前锁长期有效
                return ResponseUtil.fail("奖品已被抢光");
            }
            // TODO 减库存
            List<Object> execResults = null; // 存储事务中每个指令的返回值
            try {
                redisTemplate.setEnableTransactionSupport(true); // TODO 开启事务支持
                /*
                 * TODO 监控商品库存
                 *  事先监控库存，在提交事务时需要判断下被监控库存是否发生改变，没有变化则提交事务，否则回滚事务
                 */
                redisTemplate.watch(stockKey);
                redisTemplate.multi(); // TODO 开启事务
                redisTemplate.opsForValue().decrement(stockKey); // 商品库存递减，返回递减1后剩余的库存
                redisTemplate.opsForSet().add(winprizeKey, userId); // 记录中奖名单，添加成功返回1，否则返回0
                execResults = redisTemplate.exec(); // TODO 执行事务
            } catch (Exception e) {
                redisTemplate.discard(); // 取消事务，放弃执行事务块内的所有命令
            } finally {
                redisTemplate.unwatch(); // 取消 WATCH 命令对所有 key 的监视
                // TODO 分析事务执行结果
                if (execResults != null && execResults.size() == 2) {
                    stock = (long) execResults.get(0); // 当前剩余商品库存
                    if (stock < 0) {
                        redisTemplate.persist(lockKey); // 设置当前锁长期有效
                        return ResponseUtil.success("奖品已被抢光");
                    } else {
                        deleteLockKey(redisTemplate, Thread.currentThread().getName());
                        // TODO 【异步】记录抽奖信息(数据格式：用户编号|剩余奖品数量|中奖时间)
                        applicationContext.publishEvent(new HkSeckillEvent(this, new HkSeckillEntity(userId, Integer.parseInt(String.valueOf(stock)))));
                        return ResponseUtil.success("恭喜，您被奖品砸中！");
                    }
                } else {
                    deleteLockKey(redisTemplate, Thread.currentThread().getName());
                    return ResponseUtil.success("差点被奖品砸中，继续努力");
                }
            }
        } else {
            return ResponseUtil.fail(stock > 0 ? "差点被奖品砸中，再来一次" : "奖品已被抢光");
        }
    }

}

